#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/stat.h>
#include <linux/kdev_t.h>
#include <linux/moduleparam.h>
#include <linux/types.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>

#include "scull.h"
#include "ioctl_cmds.h"
#include "log.h"

struct scull_dev *g_dev = NULL;
dev_t g_devno;

int scull_qset = SCULL_QSET / 10;
module_param_named(qset, scull_qset, uint, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);

int scull_quantum = SCULL_QUANTUM / 10;
module_param_named(quantum, scull_quantum, uint, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));

static int scull_open(struct inode *inode, struct file *filp) {
    LOGI("open");
    struct scull_dev *sdev;

    sdev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp->private_data = sdev;
    return 0;
}

static ssize_t scull_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    LOGI("ioctl cmd 0x%x", cmd);
    int tmp = 0;
    int err = 0;

    if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;
    if (_IOC_NR(cmd) >= SCULL_IOC_MAXNR) return -ENOTTY;

    if (_IOC_DIR(cmd)) {
        // access_ok() return true when succeed.
        err = !access_ok((void __user *)arg, _IOC_SIZE(cmd));
    }
    if (err) return -EFAULT;

    if (!capable(CAP_SYS_ADMIN)) {
        return -EPERM;
    }

    switch (cmd) {
        case SCULL_IOCRESET:
            scull_qset = SCULL_QSET;
            scull_quantum = SCULL_QUANTUM;
            break;
        case SCULL_IOCSQSET:
            err = __get_user(scull_qset, (int __user *)arg);
            break;
        case SCULL_IOCSQUANTUM:
            err = __get_user(scull_quantum, (int __user *)arg);
            break;
        case SCULL_IOCTQSET:
            scull_qset = (int)arg;
            break;
        case SCULL_IOCTQUANTUM:
            scull_quantum = (int)arg;
            break;
        case SCULL_IOCGQSET:
            err = __put_user(scull_qset, (int __user *)arg);
            break;
        case SCULL_IOCGQUANTUM:
            err = __put_user(scull_quantum, (int __user *)arg);
            break;
        case SCULL_IOCQQSET:
            return scull_qset;
        case SCULL_IOCQQUANTUM:
            return scull_quantum;
        case SCULL_IOCXQSET:
            tmp = scull_qset;
            err = __get_user(scull_qset, (int __user *)arg);
            if (!err) {
                return tmp;
            }
            break;
        case SCULL_IOCXQUANTUM:
            tmp = scull_quantum;
            err = __get_user(scull_quantum, (int __user *)arg);
            if (!err) {
                return tmp;
            }
            break;
        case SCULL_IOCHQSET:
            tmp = scull_qset;
            scull_qset = (int)arg;
            return tmp;
        case SCULL_IOCHQUANTUM:
            tmp = scull_quantum;
            scull_quantum = (int)arg;
            return tmp;
        default:
            return -ENOTTY;
    }
    if (err) {
        LOGE("ioctl error: %d", err);
    }
    return err;
}

const struct file_operations scull_fops = {
    .owner = THIS_MODULE,
    .open = scull_open,
    .unlocked_ioctl = scull_ioctl,
};

static int __init scull_init(void) {
    LOGI("init");
    struct scull_dev *sdev;
    char buf[32];
    int err = 0;

    sdev = kmalloc(sizeof(struct scull_dev), GFP_KERNEL);
    if (!sdev) {
        LOGE("alloc scull dev");
        return -ENOMEM;
    }
    memset(sdev, 0, sizeof(struct scull_dev));
    cdev_init(&sdev->cdev, &scull_fops);

    err = scull_dev_sysfs_init(sdev);
    if (err) {
        return -EFAULT;
    }

    err = alloc_chrdev_region(&g_devno, 0, 1, SCULL_NAME);
    if (err) {
        LOGE("alloc scull dev no");
        return -EFAULT;
    }
    err = cdev_add(&sdev->cdev, g_devno, 1);
    if (err) {
        LOGE("add scull dev");
        return -EFAULT;
    }
    format_dev_t(buf, g_devno);
    LOGI("add scull dev at %s", buf);

    return 0;
}
module_init(scull_init);

static void __exit scull_exit(void) {
    LOGI("exit");
    if (g_dev) {
        (void)scull_dev_sysfs_deinit(g_dev);
        cdev_del(&g_dev->cdev);
        unregister_chrdev_region(g_devno, 1);
        g_dev = NULL;
    }
}
module_exit(scull_exit);

MODULE_LICENSE("GPL");